﻿using System;
using System.Collections.Generic;
using System.Linq;

using btmerc.API.Enums;
using btmerc.API.Schemas;
using btmerc.API.Schemas.Interfaces;

namespace btmerc.API.Negotiation
{
    public enum ContractChangeType
    {
        PaymentMultiplier,
        Duration,
        Command,
        Overhead,
        Salvage,
        Support,
        Transport
    }

    public class SupportChange
    {
        public float NewBattleSupport { get; set; }
        public float NewStraightSupport { get; set; }
        public double Points { get; set; }
        public double Value { get; set; }
        public SupportChange(float newBattleSupport, float newStraightSupport, double value, double points)
        {
            NewBattleSupport = newBattleSupport;
            NewStraightSupport = newStraightSupport;
            Value = value;
            Points = points;
        }
    }

    public class TransportChange
    {
        public float NewOwnedPercentage { get; set; }
        public float NewCharteredPercentage { get; set; }
        public double Points { get; set; }
        public double Value { get; set; }
        public TransportChange(float newOwned, float newChartered, double value, double points)
        {
            NewCharteredPercentage = newChartered;
            NewOwnedPercentage = newOwned;
            Value = value;
            Points = points;
        }
    }

    public class ModifiedContract
    {
        public NegotiatedContract NewContract { get; set; }
        public double PointChange { get; set; }
        public ContractChangeType ChangeType { get; set; }
        public ModifiedContract(NegotiatedContract contract)
        {
            // for now, I don't think we need to do a deep copy of the contract
            NewContract = contract;
        }
    }

    public class Negotiation
    {
        #region Private Fields
        private NegotiatedContract _contract;
        private IForce _force;
        private IEra _era;
        private MaintenanceCosts _maintenanceCosts;
        private int _mercNegotiatorSkillBonus;
        private int _houseNegotiatorSkillBonus;
        private double _bargainPool;
        private double _monthlySalary;
        private double _monthlyStraightSupportCost;
        private double _transportCharterCost;
        private double _transportOwnedCost;
        private double _monthlyCombatLiklihood;
        #endregion

        public double RemainingBargainPool
        {
            get
            {
                return _bargainPool;
            }
        }

        public Negotiation(Contract contract, DragoonsRating rating, Quality mercNegotiatorSkill, bool isFreelanceNegotiator, 
            IEra era, SalaryModifiers salaryModifiers, MaintenanceCosts maintenanceCosts, IEnumerable<Dropship> availableDropships)
        {
            _contract = new NegotiatedContract(contract);
            _force = rating._force;
            _era = era;
            _maintenanceCosts = maintenanceCosts;
            InitializeNegotiators(contract.Employer, mercNegotiatorSkill, isFreelanceNegotiator);
            InitializeBargainPool(rating, _force.HiringHall);

            // evaluate the costs of each element
            _monthlySalary = _force.GetTotalSalary(_era, salaryModifiers);
            _monthlyStraightSupportCost = _force.GetTotalMaintenanceCosts(_era, _maintenanceCosts);
            _monthlyCombatLiklihood = CalculateCombatLiklihood(contract);
            TroopCount mercCapacity = _force.GetTotalTroopTransportCapacity();
            TroopCount mercNeed = _force.GetTroopTransportNeeded();
            // TODO: for now, we're assuming single-jump travel to the mission. We should figure out how to vary this reasonably
            _transportCharterCost = CalculateCharterTransportCosts(1, mercCapacity, mercNeed, availableDropships);
            _transportOwnedCost = CalculateOwnedTransportCosts(1);

            if (_houseNegotiatorSkillBonus > _mercNegotiatorSkillBonus)
            {
                HouseNegotiates();
            }
        }

        private double CalculateCombatLiklihood(Contract contract)
        {
            switch (contract.ContractType)
            {
                case ContractType.CadreDuty:
                case ContractType.GarrisonDuty:
                    return 0.01;
                case ContractType.Espionage:
                case ContractType.Observation:
                case ContractType.Retainer:
                case ContractType.SecurityDuty:
                    return 0.1;
                case ContractType.ReconRaid:
                case ContractType.RiotDuty:
                    return 0.25;
                case ContractType.Assassination:
                case ContractType.ExtractionRaid:
                case ContractType.ObjectiveRaid:
                case ContractType.Sabotage:
                case ContractType.Terrorism:
                    return 0.33;
                case ContractType.Diversion:
                case ContractType.MoleHunting:
                case ContractType.PirateHunting:
                case ContractType.ReliefDuty:
                    return 0.5;
                case ContractType.Guerrilla:
                case ContractType.PlanetaryAssault:
                case ContractType.PlanetaryAssaultDiversionary:
                case ContractType.PlanetaryAssaultGuerrilla:
                case ContractType.PlanetaryAssaultReconRaid:
                    return 1;
                default: return 0;
            }
        }

        private void InitializeBargainPool(DragoonsRating rating, string hiringHall)
        {
            _bargainPool = rating.Score;
            double percentage = 1;
            if (hiringHall != "Outreach" && hiringHall != "Galatea")
            {
                percentage = 0.95;
            }
            percentage += (_mercNegotiatorSkillBonus - _houseNegotiatorSkillBonus) * 0.1;
            if (_force.GetLengthOfDebt() > 0)
            {
                percentage -= Math.Ceiling(_force.GetLengthOfDebt() / 12.0) * 0.1;
            }
            else if (_force.CurrentBalance <= 0)
            {
                percentage -= 0.1;
            }
            _bargainPool = Math.Round(_bargainPool * percentage);
        }

        private void InitializeNegotiators(Affiliation employer, Quality mercNegotiatorSkill, bool isFreelanceNegotiator)
        {
            switch (mercNegotiatorSkill)
            {
                case Quality.Green:
                    _mercNegotiatorSkillBonus = 1;
                    break;
                case Quality.Regular:
                    _mercNegotiatorSkillBonus = 3;
                    break;
                case Quality.Veteran:
                    _mercNegotiatorSkillBonus = 5;
                    break;
                case Quality.Elite:
                    _mercNegotiatorSkillBonus = 7;
                    break;
                default:
                    _mercNegotiatorSkillBonus = 0;
                    break;
            }
            _houseNegotiatorSkillBonus = _mercNegotiatorSkillBonus;
            if (isFreelanceNegotiator)
            {
                _mercNegotiatorSkillBonus--;
            }
            switch (employer)
            {
                case Affiliation.FederatedSuns:
                case Affiliation.DraconisCombine:
                case Affiliation.WordofBlake:
                    _houseNegotiatorSkillBonus--;
                    break;
                case Affiliation.LyranCommonwealth:
                case Affiliation.Periphery:
                    _houseNegotiatorSkillBonus++;
                    break;
            }
        }

        //TODO: eventually, we should give negotiators more personality
        // For now, we'll have a pretty generic negotiator
        private ModifiedContract HouseNegotiates()
        {
            // determine a point to cost ratio for each element
            // TODO: figure out a way to cost command rating
            // TODO: determine whether it's worth seizing the agenda
            double bestEfficiency = 10000000;
            int pointChange = 0;
            TransportChange bestTransportChange = null;
            SupportChange support = null;
            ModifiedContract modifiedContract = new ModifiedContract(_contract);
            modifiedContract.ChangeType = ContractChangeType.Command;
            
            if (!_contract.IsPayDiscussed)
            {
                // 5 point decrease in pool per 0.2 increase in contract multiplier
                double increasePayCostPerPoint = _contract.ContractTerms.Duration * _monthlySalary * 0.04;
                if (increasePayCostPerPoint < bestEfficiency)
                {
                    modifiedContract.ChangeType = ContractChangeType.PaymentMultiplier;
                }
            }

            if (!_contract.IsOverheadDiscussed && _contract.ContractTerms.OverheadCompensation != 1)
            {
                // 2.5 points per none -> half -> full
                double increaseOverheadCostPerPoint = _contract.ContractTerms.Duration * _monthlySalary * 0.01;
                if (increaseOverheadCostPerPoint < bestEfficiency)
                {
                    modifiedContract.ChangeType = ContractChangeType.Overhead;
                }
            }

            if (!_contract.IsSupportDiscussed)
            {
                support = GetBestSupportChange();
                if (support != null && support.Value < bestEfficiency)
                {
                    modifiedContract.ChangeType = ContractChangeType.Support;
                }
            }

            // TODO: salvage logic
            double increaseSalvageCostPerPoint = 0;

            if (!_contract.IsTransportDiscussed)
            {
                bestTransportChange = GetBestTransportChange();
                if (bestTransportChange != null && bestTransportChange.Value < bestEfficiency)
                {
                    modifiedContract.ChangeType = ContractChangeType.Transport;
                }
            }

            switch (modifiedContract.ChangeType)
            {
                case ContractChangeType.Overhead:
                    // this means we should get to full overhead coverage
                    modifiedContract.NewContract.IsOverheadDiscussed = true;
                    modifiedContract.NewContract.ContractTerms.OverheadCompensation = 1;
                    if (_contract.ContractTerms.OverheadCompensation == 0)
                    {
                        modifiedContract.PointChange = -5;
                    }
                    else
                    {
                        modifiedContract.PointChange = -2.5;
                    }
                    break;
                case ContractChangeType.PaymentMultiplier:
                    // since payment is conceivably uncapped, we should figure out how much we're willing to pump payment
                    double maxIncrease = _bargainPool * 0.2 / 5.0;
                    modifiedContract.NewContract.IsPayDiscussed = true;
                    modifiedContract.NewContract.ContractTerms.PaymentMultiplier += (float)maxIncrease;
                    break;
                case ContractChangeType.Support:
                    modifiedContract.NewContract.IsSupportDiscussed = true;
                    modifiedContract.PointChange = support.Points * -1;
                    modifiedContract.NewContract.ContractTerms.BattleSupportPercentage = support.NewBattleSupport;
                    modifiedContract.NewContract.ContractTerms.StraightSupportPercentage = support.NewStraightSupport;
                    break;
                case ContractChangeType.Transport:
                    modifiedContract.NewContract.IsTransportDiscussed = true;
                    modifiedContract.PointChange = support.Points * -1;
                    modifiedContract.NewContract.ContractTerms.TransportCompensationChartered = bestTransportChange.NewCharteredPercentage;
                    modifiedContract.NewContract.ContractTerms.TransportCompensationOwned = bestTransportChange.NewOwnedPercentage;
                    break;
                case ContractChangeType.Duration:
                case ContractChangeType.Command:
                case ContractChangeType.Salvage:
                default:
                    throw new ArgumentOutOfRangeException("You ended up with an unsupported negotiated point!");
            }

            return null;
        }

        private SupportChange GetBestSupportChange()
        {
            double straightCostPerPoint, battleCostPerPoint;
            float straight = _contract.ContractTerms.StraightSupportPercentage;
            float battle = _contract.ContractTerms.BattleSupportPercentage;
            
            double currentStraightCost = _contract.ContractTerms.Duration * _contract.ContractTerms.StraightSupportPercentage * _monthlyStraightSupportCost;
            double straightCost = _contract.ContractTerms.Duration * _monthlyStraightSupportCost;
            double battleCost = _contract.ContractTerms.Duration * _monthlyCombatLiklihood;

            // TODO: we need to factor in the average repair cost or the like; 
            // should we just measure as a percentage of the total value of the force?
            double currentBattleCost = _contract.ContractTerms.Duration * _contract.ContractTerms.BattleSupportPercentage * _monthlyCombatLiklihood;
            List<SupportChange> possibleChanges = new List<SupportChange>();

            if (straight == 0 && battle == 0)
            {
                // switching leads to odd behavior if percentages default to 0
                // let's assume they have to offer at least 10%
                if (_bargainPool >= 10)
                {
                    possibleChanges.Add(new SupportChange(0, 0.1F, straightCost * 0.01, 10));
                }
                if (_bargainPool >= 15)
                {
                    possibleChanges.Add(new SupportChange(0.1F, 0, battleCost / 150, 15));
                }
                if (_bargainPool >= 28.5)
                {
                    possibleChanges.Add(new SupportChange(0.1F, 0.1F, (battleCost + straightCost) * 0.1 / 28.5, 28.5));
                }                
            }
            else if (battle == 0)
            {
                // straight support already offered
                if (straight < 1)
                {
                    straightCostPerPoint = straightCost * 0.02;
                    if ((1 - straight) * 50 <= _bargainPool)
                    {
                        possibleChanges.Add(new SupportChange(0, 1, straightCostPerPoint, 50 * (1 - straight)));
                    }
                    else
                    {
                        double pointsLeft = Math.Floor(_bargainPool);
                        possibleChanges.Add(new SupportChange(0F, (float)(straight + (pointsLeft / 50.0F)), straightCostPerPoint, pointsLeft));
                    }
                }
                if (_bargainPool >= 10)
                {
                    possibleChanges.Add(new SupportChange(0.1F, 0, ((battleCost * 0.1) - currentStraightCost) / 10.0, 10));
                }
                if (_bargainPool >= 20.5)
                {
                    possibleChanges.Add(new SupportChange(0.1F, straight, battleCost * 0.1 / 20.5, 20.5));
                }
            }
            else if (straight == 0)
            {
                // battle support offered
                if (battle < 0.75)
                {
                    battleCostPerPoint = battleCost * 0.02;
                    if ((0.75 - battle) * 50 <= _bargainPool)
                    {
                        possibleChanges.Add(new SupportChange(0.75F, 0, battleCostPerPoint, 50 * (0.75 - battle)));
                    }
                    else
                    {
                        double pointsLeft = Math.Floor(_bargainPool);
                        possibleChanges.Add(new SupportChange((float)(battle + (pointsLeft / 100)), 0, battleCostPerPoint, pointsLeft));
                    }
                }
                if (_bargainPool >= 2.5)
                {
                    possibleChanges.Add(new SupportChange(0, 0.15F, ((straightCost * 0.15) - currentBattleCost) / 2.5, 2.5));
                }
                if (_bargainPool >= 15.5)
                {
                    possibleChanges.Add(new SupportChange(battle, 0.1F, straightCost * 0.1 / 15.5, 15.5));
                }
                if (_bargainPool >= 45)
                {
                    possibleChanges.Add(new SupportChange(0, 1, (straightCost - currentBattleCost) / 45, 45));
                }
            }
            else
            {
                // both forms of support already offered
                if (straight < 1)
                {
                    straightCostPerPoint = straightCost / 30;
                    if ((1 - straight) * 30 <= _bargainPool)
                    {
                        possibleChanges.Add(new SupportChange(battle, 1, straightCostPerPoint, 30 * (1 - straight)));
                    }
                    else
                    {
                        double pointsLeft = Math.Floor(_bargainPool);
                        possibleChanges.Add(new SupportChange(battle, (float)(straight + (pointsLeft / 30)), straightCostPerPoint, pointsLeft));
                    }
                }
                if(_contract.ContractTerms.BattleSupportPercentage < .75)
                {
                    battleCostPerPoint = battleCost / 30;
                    if ((0.75 - battle) * 30 <= _bargainPool)
                    {
                        possibleChanges.Add(new SupportChange(1, straight, battleCostPerPoint, 30 * (0.75 - battle)));
                    }
                    else
                    {
                        double pointsLeft = Math.Floor(_bargainPool);
                        possibleChanges.Add(new SupportChange((float)(battle + (pointsLeft / 30)), straight, battleCostPerPoint, pointsLeft));
                    }
                }
            }
            return possibleChanges.OrderBy(x => x.Value).FirstOrDefault();
        }

        private TransportChange GetBestTransportChange()
        {
            if (_contract.ContractTerms.TransportCompensationChartered == 0)
            {
                // see whether partial or full is more cost efficient
                TransportChange partial = new TransportChange(0.1F, 0.1F, (_transportCharterCost + _transportOwnedCost) * 0.1 / 13, 13);
            }
            else if(_contract.ContractTerms.TransportCompensationChartered < 1)
            {
                // TODO: see if there's a benefit to just partial increases, rather than moving to full
                double charterCostChange = (1 - _contract.ContractTerms.TransportCompensationChartered) * _transportCharterCost;
                double ownedCostChange = (1 - _contract.ContractTerms.TransportCompensationOwned) * _transportOwnedCost;
                double fullCostPerPoint = (charterCostChange + ownedCostChange) / 15;
                return new TransportChange(1, 1, fullCostPerPoint, 15);
            }
            return null;
        }

        private double CalculateCharterTransportCosts(ushort jumpCount, TroopCount capacity, TroopCount need, IEnumerable<Dropship> availableDropships)
        {
            List<Dropship> dropshipsNeeded = GetDropshipsNeeded(capacity, need, availableDropships);
            // add to the list the aux dropships of the merc force
            dropshipsNeeded.AddRange(_force.GetAuxillaryDropships().Dropship);
            double dropShipCost = dropshipsNeeded.Sum(d => d.BaseHireCost);
            double jumpShipCost = 100000 * jumpCount * dropshipsNeeded.Count();
            return dropShipCost + jumpShipCost;
        }

        private double CalculateOwnedTransportCosts(ushort jumpCount)
        {
            Dropships dropships = _force.GetOwnedDropships();
            double jumpShipCost = 100000 * jumpCount * dropships.Dropship.Count();
            double dropshipCost = dropships.Dropship.Sum(d => d.BaseHireCost);
            return dropshipCost + jumpShipCost;
        }

        private static List<Dropship> GetDropshipsNeeded(TroopCount capacity, TroopCount need, IEnumerable<Dropship> availableDropships)
        {
            // determine remaining capacity need
            TroopCount leftover = new TroopCount(need, capacity);

            // determine optimal coverage of remaining need
            // TODO: ignoring cargo for the time being
            // iterate through each available dropship, see what it's cost/troop is, 
            double bestCostPerTroop = 1000000;
            Dropship bestDropship = null;
            double costPerTroop;
            int troopsFit;
            List<Dropship> dropshipsNeeded = new List<Dropship>();
            while (leftover.HasTroops())
            {
                foreach (Dropship dropship in availableDropships)
                {
                    troopsFit = GetFittingTroopCount(leftover, dropship);
                    costPerTroop = dropship.BaseHireCost / (double)troopsFit;
                    if (costPerTroop < bestCostPerTroop)
                    {
                        bestDropship = dropship;
                        bestCostPerTroop = costPerTroop;
                    }
                }
                dropshipsNeeded.Add(bestDropship);
                leftover = new TroopCount(leftover, bestDropship.StorageCapacity);
            }
            return dropshipsNeeded;
        }

        private static int GetFittingTroopCount(TroopCount leftover, Dropship dropship)
        {
            int troopsFit;
            troopsFit = leftover.AeroCapacity > dropship.StorageCapacity.AeroCapacity ? dropship.StorageCapacity.AeroCapacity : leftover.AeroCapacity;
            troopsFit += leftover.BattleArmorCapacity > dropship.StorageCapacity.BattleArmorCapacity ? dropship.StorageCapacity.BattleArmorCapacity : leftover.BattleArmorCapacity;
            troopsFit += leftover.BattlemechCapacity > dropship.StorageCapacity.BattlemechCapacity ? dropship.StorageCapacity.BattlemechCapacity : leftover.BattlemechCapacity;
            troopsFit += leftover.HeavyVehicleCapacity > dropship.StorageCapacity.HeavyVehicleCapacity ? dropship.StorageCapacity.HeavyVehicleCapacity : leftover.HeavyVehicleCapacity;
            troopsFit += leftover.TroopCapacity > dropship.StorageCapacity.TroopCapacity ? dropship.StorageCapacity.TroopCapacity : leftover.TroopCapacity;
            troopsFit += leftover.VehicleCapacity > dropship.StorageCapacity.VehicleCapacity ? dropship.StorageCapacity.VehicleCapacity : leftover.VehicleCapacity;
            return troopsFit;
        }
    }
}
